/*******************************************************************************
 *
 *      Filename: GLWindow.cpp
 *   Description: See GLWindow.h
 *        Author: [kb]ypp
 *    Created at: November 19, 2005
 * Last modified: December 24, 2005
 *
 ******************************************************************************/

#include "GLWindow.h"
#include "Application.h"
#include "Controller.h"
#include "Log.h"
#include <cassert>
#include <stdexcept>
#include <string>
using namespace std;

#ifdef _WIN32

/*******************************************************************************
 *
 * GLWindowImpl & GLWindow for MS Windows
 *
 ******************************************************************************/

// Windows-specific code here.
// ...

#else // _WIN32

/*******************************************************************************
 *
 * GLWindowImpl & GLWindow for the X Window System (Linux)
 *
 ******************************************************************************/

#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/extensions/xf86vmode.h>
#include <X11/keysym.h>
#include <GL/glx.h>

//! @brief Struct to hold OS-dependent implementation data for GLWindow
//!
//! X Window System version (Linux).
struct GLWindow::GLWindowImpl {
	Display *display;
	GLXFBConfig *configs;
	XVisualInfo *vinfo;
	Colormap colormap;
	Window window;
	GLXWindow glxwindow;
	GLXContext context;
	Atom wmDeleteWindow;
	XF86VidModeModeInfo **modes;
	int modes_count;

	//! @brief Default constructor
	GLWindowImpl();
	//! @brief Destructor
	~GLWindowImpl();
	//! @brief Check if GLX extension is supported by `display'
	bool haveGLX();
	//! @brief Initialize GLX extension
	void initGLX();
	//! @brief Free all resources
	void cleanup();
};

//! Initialize all member data with zeroes.
//!
GLWindow::GLWindowImpl::GLWindowImpl()
	: display( 0 ), configs( 0 ), vinfo( 0 ), colormap( 0 ),
	  window( 0 ), glxwindow( 0 ), context( 0 ), wmDeleteWindow( None ),
	  modes( 0 ), modes_count( 0 )
{
}

//! Just call the cleanup() function.
//!
GLWindow::GLWindowImpl::~GLWindowImpl()
{
	cleanup();
}

//! If GLX extension is not supported (or have version < 1.3)
//! function reports log error and retruns `false'. If everything ok,
//! it returns `true'.
bool GLWindow::GLWindowImpl::haveGLX()
{
	// Ascertain if the GLX extension is defined for an X server:
	if ( glXQueryExtension( display, 0, 0 ) == False ) {
		LOG_ERROR( "X server has no OpenGL GLX extension" );
		return false;
	}
	
	// Discover which version of GLX is available (>= 1.3 needed):
	int major, minor;
	glXQueryVersion( display, &major, &minor );
	if ( !( major == 1 && minor >= 3 ) ) {
		LOG_ERROR( "GLX version 1.3 or higher needed" );
		return false;
    }
	
	return true;
}

//! If any initialization routine fails, runtime_error exception thrown.
//!
void GLWindow::GLWindowImpl::initGLX()
{
	// Make sure the X window is created normally:
	assert( display && configs && window );
	
	// Create a GLX window:
	glxwindow = glXCreateWindow( display, configs[ 0 ], window, 0 );
	if ( !window ) {
		LOG_ERROR( "Can't create GLX window" );
		throw runtime_error( "Can't create GLX window" );
	}
	
	// Create an OpenGL rendering context:
	context = glXCreateNewContext(
		display, configs[ 0 ], GLX_RGBA_TYPE, 0, True );
	if ( !context ) {
		LOG_ERROR( "Could not create rendering context" );
		throw runtime_error( "Could not create rendering context" );
	}
	if ( glXIsDirect( display, context ) == False )
		LOG_WARNING( "Created rendering context is indirect"
					 "(bad performance expected!)" );
	// Make it current:
	if ( glXMakeContextCurrent( display, glxwindow, glxwindow, context )
		== False ) {
		LOG_ERROR( "Failed to attach GLX context to the window" );
		throw runtime_error( "Failed to attach GLX context to the window" );
	}
}

void GLWindow::GLWindowImpl::cleanup()
{
	if ( modes ) {
		XF86VidModeSwitchToMode( display, DefaultScreen( display ), modes[ 0 ] );
		XFlush( display );
		XFree( modes );
	}
	
	if ( context ) {
		glXMakeContextCurrent( display, None, None, 0 );
		glXDestroyContext( display, context );
		context = 0;
	}
	if ( glxwindow ) {
		glXDestroyWindow( display, glxwindow );
		glxwindow = 0;
	}
	if ( colormap ) {
		XFreeColormap( display, colormap );
		colormap = 0;
	}
	if ( window ) {
		XDestroyWindow( display, window );
		window = 0;
	}
	if ( vinfo ) {
		XFree( vinfo );
		vinfo = 0;
	}
	if ( configs ) {
		XFree( configs );
		configs = 0;
	}
	if ( display ) {
		XCloseDisplay( display );
		display = 0;
	}
}

//! Here we'll create and map an X window
//!
GLWindow::GLWindow( Application &owner, int width, int height, bool fs )
	: owner_( owner ), title_( "" ), width_( width ), height_( height ),
	  fullscreen_( fs ), impl_( new GLWindowImpl )
{
	try {
		// Connect to the X server:
		impl_->display = XOpenDisplay( 0 );
		if ( !impl_->display ) {
			LOG_EMERGENCY( "Could not open X display" );
			throw runtime_error( "Could not open X display" );
		}
		
		// FIXME: Refactor it later!
		if ( !impl_->haveGLX() )
			throw runtime_error( "GLX extension is not supported" );
		
		if ( fs ) {
			if ( !XF86VidModeGetAllModeLines( impl_->display,
				DefaultScreen( impl_->display ),
				&impl_->modes_count, &impl_->modes )
			)
				throw runtime_error( "Could not get XF86VidMode mode lines" );
			
			int best = -1;
			for ( int i = impl_->modes_count - 1; i >= 0; --i )
				if ( impl_->modes[ i ]->hdisplay == width_ &&
					 impl_->modes[ i ]->vdisplay == height_
				)
					best = i;
			if ( best >= 0 ) {
				XF86VidModeSwitchToMode( impl_->display,
				DefaultScreen( impl_->display ), impl_->modes[ best ] );
				XFlush( impl_->display );
			} else
			 	throw runtime_error(
					"Could not find video mode with given width/height" );
		}
		
		// Selection of the GLXFBConfig:
		const int attrib_list[] = {
			GLX_BUFFER_SIZE,      32,
			GLX_DOUBLEBUFFER,     True,
			GLX_RED_SIZE,         8,
			GLX_GREEN_SIZE,       8,
			GLX_BLUE_SIZE,        8,
			GLX_ALPHA_SIZE,       8,
			GLX_DEPTH_SIZE,       24,
			/*GLX_STENCIL_SIZE,   8, // May be used later!
			GLX_ACCUM_RED_SIZE,   16,
			GLX_ACCUM_GREEN_SIZE, 16,
			GLX_ACCUM_BLUE_SIZE,  16,
			GLX_ACCUM_ALPHA_SIZE, 16,*/
			GLX_DRAWABLE_TYPE,    GLX_WINDOW_BIT,
			GLX_X_RENDERABLE,     True,
			GLX_X_VISUAL_TYPE,    GLX_TRUE_COLOR,
			GLX_CONFIG_CAVEAT,    GLX_NONE,
			None
		};
		int nconfigs = 0;
		impl_->configs = glXChooseFBConfig( impl_->display,
			DefaultScreen( impl_->display ), attrib_list, &nconfigs );
		// FIXME: May be use of `nconfigs' will be better than `configs'?
		if ( !impl_->configs ) {
			LOG_EMERGENCY( "Can't find appropriate GLXFBConfig" );
			throw runtime_error( "Can't find appropriate GLXFBConfig" );
		}
		impl_->vinfo = glXGetVisualFromFBConfig(
			impl_->display, impl_->configs[ 0 ] );
		/*
		FIXME: Try this:
		if ( impl_->vinfo->class != TrueColor )
			throw runtime_error( "TrueColor visual required for this program" );
		*/
		if ( !impl_->vinfo ) {
			LOG_EMERGENCY( "Failed to select appropriate visual" );
			throw runtime_error( "Filed to select appropriate visual" );
		}
		
		// Create a color map for a new window:
		impl_->colormap = XCreateColormap( impl_->display,
			RootWindow( impl_->display, impl_->vinfo->screen ),
			impl_->vinfo->visual, AllocNone );
		
		// Create an X window:
		XSetWindowAttributes attrib;
		attrib.background_pixel = 0;
		attrib.border_pixel = 0;
		attrib.colormap = impl_->colormap;
		attrib.event_mask = KeyPressMask | KeyReleaseMask |
			ButtonPressMask | ButtonReleaseMask | PointerMotionMask |
			ExposureMask | VisibilityChangeMask | StructureNotifyMask;
		unsigned long valuemask = CWBackPixel | CWBorderPixel |
			CWColormap | CWEventMask;
		
		if ( fs ) {
			attrib.backing_store = NotUseful;
			attrib.save_under = False;
			attrib.override_redirect = True;
			valuemask |= CWBackingStore | CWSaveUnder | CWOverrideRedirect;
		}
		
		impl_->window = XCreateWindow( impl_->display,
			RootWindow( impl_->display, impl_->vinfo->screen ),
			0, 0, width_, height_,
			0, impl_->vinfo->depth, InputOutput, impl_->vinfo->visual,
			valuemask, &attrib );
		// Map window to the screen (make it visible):
		XMapWindow( impl_->display, impl_->window );
		/*
		TRY THIS:
		XSetStandardProperties( dpy, win, "glxsimple", "glxsimple", None, argv, argc, NULL );
		*/
		
		if ( fs ) {
			XMoveWindow( impl_->display, impl_->window, 0, 0 );
			XRaiseWindow( impl_->display, impl_->window );
			XFlush( impl_->display );
			XF86VidModeSetViewPort( impl_->display,
				DefaultScreen( impl_->display ), 0, 0 );
			XGrabPointer( impl_->display, impl_->window, True, 0, GrabModeAsync,
				GrabModeAsync, impl_->window, None, CurrentTime );
			XGrabKeyboard( impl_->display, impl_->window, True, GrabModeAsync,
				GrabModeAsync, CurrentTime );
		} else {
			// Registering window destroy event:
			// QUESTION: Should the wmDeleteWindow atom be freed;
			impl_->wmDeleteWindow = XInternAtom( impl_->display,
				"WM_DELETE_WINDOW",	False );
			XSetWMProtocols( impl_->display, impl_->window, &impl_->wmDeleteWindow, 1 );
		}
		XFlush( impl_->display );
		
		impl_->initGLX();
		
	} catch ( ... ) {
		delete impl_;
		throw;
	}
}

GLWindow::~GLWindow()
{
	delete impl_;
}

void GLWindow::title( const string &ttl )
{
	title_ = ttl;
	XStoreName( impl_->display, impl_->window, title_.c_str() );
	XSetIconName( impl_->display, impl_->window, title_.c_str() );
}

static int translateKey( XKeyEvent &event )
{
	int key = XLookupKeysym( &event, 0 );
	int res = Keyboard::KEY_NONE;
	if ( key & 0xff00 ) // Special key?
		switch ( key ) {
			case XK_Escape:    res = Keyboard::KEY_ESCAPE;    break;
			case XK_Return:    res = Keyboard::KEY_ENTER;     break;
			case XK_Tab:       res = Keyboard::KEY_TAB;       break;
			case XK_Shift_L:   res = Keyboard::KEY_LSHIFT;    break;
			case XK_Control_L: res = Keyboard::KEY_LCONTROL;  break;
			case XK_Alt_L:     res = Keyboard::KEY_LALT;      break;
			case XK_Caps_Lock: res = Keyboard::KEY_CAPS_LOCK; break;
			case XK_Left:      res = Keyboard::KEY_LEFT;      break;
			case XK_Up:        res = Keyboard::KEY_UP;        break;
			case XK_Right:     res = Keyboard::KEY_RIGHT;     break;
			case XK_Down:      res = Keyboard::KEY_DOWN;      break;
			case XK_F1:        res = Keyboard::KEY_F1;        break;
			case XK_F2:        res = Keyboard::KEY_F2;        break;
			case XK_F3:        res = Keyboard::KEY_F3;        break;
			case XK_F4:        res = Keyboard::KEY_F4;        break;
			case XK_F5:        res = Keyboard::KEY_F5;        break;
			case XK_F6:        res = Keyboard::KEY_F6;        break;
			case XK_F7:        res = Keyboard::KEY_F7;        break;
			case XK_F8:        res = Keyboard::KEY_F8;        break;
			case XK_F9:        res = Keyboard::KEY_F9;        break;
			case XK_F10:       res = Keyboard::KEY_F10;       break;
			case XK_F11:       res = Keyboard::KEY_F11;       break;
			case XK_F12:       res = Keyboard::KEY_F12;       break;
			case XK_Insert:    res = Keyboard::KEY_INSERT;    break;
			case XK_Delete:    res = Keyboard::KEY_DELETE;    break;
			case XK_Home:      res = Keyboard::KEY_HOME;      break;
			case XK_End:       res = Keyboard::KEY_END;       break;
			case XK_Page_Up:   res = Keyboard::KEY_PAGE_UP;   break;
			case XK_Page_Down: res = Keyboard::KEY_PAGE_DOWN; break;
			case XK_BackSpace: res = Keyboard::KEY_BACKSPACE; break;
			case XK_Shift_R:   res = Keyboard::KEY_RSHIFT;    break;
			case XK_Control_R: res = Keyboard::KEY_RCONTROL;  break;
			case XK_Alt_R:     res = Keyboard::KEY_RALT;      break;
			default:
				//???
				break;
		}
	else
		res = key;

	return res;
}

static int translateButton( unsigned int mbutton )
{
	int btn = 0;
	switch ( mbutton ) {
		case Button1:
			btn = Mouse::MB_LEFT;
			break;
		case Button2:
			btn = Mouse::MB_MIDDLE;
			break;
		case Button3:
			btn = Mouse::MB_RIGHT;
			break;
		case Button4:
			btn = Mouse::MB_SCROLL_UP;
			break;
		case Button5:
			btn = Mouse::MB_SCROLL_DOWN;
			break;
		default:
			LOG_WARNING( "Unknown mouse button pressed" );
			break;
	}
	return btn;
}

void GLWindow::processEvents()
{
	XEvent event;
	while ( XPending( impl_->display ) ) { // While have pending events
		XNextEvent( impl_->display, &event );
		switch ( event.type ) {
			case MotionNotify: // Mouse pointer move event
				Mouse::instance().moveCursor( event.xmotion.x, event.xmotion.y );
				break;
			case KeyPress: // Keyboard event
				Keyboard::instance().keyPress( translateKey( event.xkey ) );
				// TEMPORARY: Application must exit after Esc button pressed:
				if ( translateKey( event.xkey ) == Keyboard::KEY_ESCAPE )
					owner_.quit();
				break;
			case KeyRelease: // Keyboard event
				Keyboard::instance().keyRelease( translateKey( event.xkey ) );
				break;
			case ButtonPress: // Mouse button event
				Mouse::instance().buttonPress( translateButton( event.xbutton.button ) );
				break;
			case ButtonRelease: // Mouse button event
				Mouse::instance().buttonRelease( translateButton( event.xbutton.button ) );
				break;
			case ConfigureNotify:
				width_ = event.xconfigure.width;
				height_ = event.xconfigure.height;
				owner_.resize( width_, height_ );
				break;
			case MapNotify:
				owner_.resume();
				break;
			case UnmapNotify:
				owner_.suspend();
				break;
			case ClientMessage:
				// OLD VERSION:
				/*{
					char *atomName = XGetAtomName( impl_->display, event.xclient.message_type );
					if ( strcmp( atomName, "WM_PROTOCOLS" ) == 0 )
						owner_.quit();
					XFree( atomName );
				}*/
				// NEW VERSION:
				if ( static_cast< Atom >( event.xclient.data.l[ 0 ] ) ==
					impl_->wmDeleteWindow )
					owner_.quit();
				break;
			default: // Ignore other events
				break;
		} // case
	} // while
}

void GLWindow::swapBuffers() const
{
	glXSwapBuffers( impl_->display, impl_->glxwindow );
}

#endif // _WIN32

/*
inline void GLWindow::keyPress( int key ) const
{
	Keyboard::instance().keyPress( key );
}

inline void GLWindow::keyRelease( int key ) const
{
	Keyboard::instance().keyRelease( key );
}

inline void GLWindow::mousePress( int mbutton ) const
{
	Mouse::instance().buttonPress( mbutton );
}

inline void GLWindow::mouseRelease( int mbutton ) const
{
	Mouse::instance().buttonRelease( mbutton );
}

inline void GLWindow::mouseMove( int x, int y ) const
{
	Mouse::instance().moveCursor( x, y );
}

inline void GLWindow::resize( int width, int height ) const
{
	cout << "width: " << width << "\t" << "height: " << height << endl;
}

inline void GLWindow::minimize() const
{
	cout << "minimize" << endl;
}

inline void GLWindow::restore() const
{
	cout << "restore" << endl;
}
*/
